iT邦幫忙

1

經典小畫家白板

  • 分享至 

  • xImage
  •  

經典小畫家白板:
https://ithelp.ithome.com.tw/upload/images/20260904/201840085gzspHAQ9e.png
程式碼:

<!DOCTYPE html>
<html lang="zh-TW" class="h-full bg-slate-100">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>經典小畫家白板</title>
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- FontAwesome Icons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        /* 繪圖畫布預設為純白背景 */
        #drawCanvas {
            cursor: crosshair;
            background-color: #ffffff;
            touch-action: none;
        }
    </style>
</head>
<body class="h-full flex flex-col overflow-hidden font-sans select-none">

    <!-- 頂部功能區 (Ribbon UI 樣式) -->
    <header class="bg-slate-200 border-b border-slate-300 px-4 py-2 flex flex-col shadow-sm z-50">
        <!-- 上方標題與檔案動作列 -->
        <div class="flex items-center justify-between pb-2 border-b border-slate-300/60 mb-2">
            <div class="flex items-center gap-2">
                <i class="fa-solid fa-paintbrush text-xl text-blue-600"></i>
                <h1 class="text-base font-bold text-slate-800 tracking-tight">經典小畫家白板</h1>
            </div>
            
            <div class="flex items-center gap-2">
                <!-- 檔案上傳按鈕 (支援圖片上傳與編輯) -->
                <label for="imageInput" class="cursor-pointer flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 rounded text-xs font-medium shadow-sm transition">
                    <i class="fa-solid fa-folder-open text-blue-600"></i>
                    <span>開啟圖片</span>
                </label>
                <input type="file" id="imageInput" accept="image/*" class="hidden">

                <button id="clearBtn" class="flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-300 hover:bg-rose-50 hover:text-rose-600 text-slate-700 rounded text-xs font-medium shadow-sm transition">
                    <i class="fa-solid fa-file-circle-plus text-rose-500"></i>
                    <span>新增白板</span>
                </button>

                <button id="saveBtn" class="flex items-center gap-1.5 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium shadow-sm transition">
                    <i class="fa-solid fa-floppy-disk"></i>
                    <span>儲存</span>
                </button>
            </div>
        </div>

        <!-- 仿 Ribbon 工具列面板 (精簡為鉛筆、橡皮擦、大小、色彩調色盤) -->
        <div class="flex items-center gap-6 overflow-x-auto py-1 text-xs">
            
            <!-- 影像群組 (復原/重做) -->
            <div class="flex flex-col items-center border-r border-slate-300 pr-6">
                <div class="flex gap-1 mb-1">
                    <button id="undoBtn" disabled title="復原" class="p-2 bg-white border border-slate-300 hover:bg-slate-100 rounded disabled:opacity-40"><i class="fa-solid fa-rotate-left"></i></button>
                    <button id="redoBtn" disabled title="重做" class="p-2 bg-white border border-slate-300 hover:bg-slate-100 rounded disabled:opacity-40"><i class="fa-solid fa-rotate-right"></i></button>
                </div>
                <span class="text-[11px] text-slate-600 font-medium">歷程</span>
            </div>

            <!-- 工具群組 (僅保留鉛筆與橡皮擦) -->
            <div class="flex flex-col items-center border-r border-slate-300 pr-6">
                <div class="flex gap-2 mb-1">
                    <button id="pencilTool" title="鉛筆/畫筆" class="tool-btn flex flex-col items-center justify-center w-10 h-10 bg-blue-100 border border-blue-400 text-blue-700 rounded shadow-sm">
                        <i class="fa-solid fa-pencil text-sm"></i>
                        <span class="text-[9px] mt-0.5">鉛筆</span>
                    </button>
                    <button id="eraserTool" title="橡皮擦" class="tool-btn flex flex-col items-center justify-center w-10 h-10 bg-white border border-slate-300 hover:bg-slate-100 text-slate-700 rounded shadow-sm">
                        <i class="fa-solid fa-eraser text-sm"></i>
                        <span class="text-[9px] mt-0.5">橡皮擦</span>
                    </button>
                </div>
                <span class="text-[11px] text-slate-600 font-medium">工具</span>
            </div>

            <!-- 大小粗細群組 -->
            <div class="flex flex-col items-center border-r border-slate-300 pr-6">
                <div class="flex flex-col justify-center h-full mb-1">
                    <label for="brushSize" class="text-[10px] text-slate-600 mb-0.5">筆刷粗細: <span id="sizeText" class="font-bold">3</span>px</label>
                    <input type="range" id="brushSize" min="1" max="40" value="3" class="w-28 accent-blue-600">
                </div>
                <span class="text-[11px] text-slate-600 font-medium">大小</span>
            </div>

            <!-- 色彩調色盤群組 (含當選取外框發亮提示) -->
            <div class="flex items-center gap-4">
                <!-- 顏色方格網 -->
                <div class="grid grid-cols-10 gap-1.5" id="paletteGrid">
                    <!-- 顏色由 JavaScript 動態產生 -->
                </div>

                <!-- 自訂編輯色彩 -->
                <div class="flex flex-col items-center pl-3 border-l border-slate-300">
                    <label for="customColor" class="cursor-pointer flex flex-col items-center">
                        <div class="w-8 h-8 rounded bg-gradient-to-tr from-rose-500 via-emerald-500 to-blue-500 border border-slate-400 shadow-sm flex items-center justify-center text-white text-xs">
                            <i class="fa-solid fa-palette"></i>
                        </div>
                        <span class="text-[10px] text-slate-600 mt-0.5">編輯色彩</span>
                    </label>
                    <input type="color" id="customColor" value="#000000" class="hidden">
                </div>
            </div>

        </div>
    </header>

    <!-- 繪圖白板主區域 -->
    <main class="flex-1 bg-slate-400 p-6 flex justify-center items-center overflow-auto">
        <div class="bg-white shadow-2xl border border-slate-500 relative flex items-center justify-center" id="canvasWrapper">
            <canvas id="drawCanvas" width="900" height="550"></canvas>
        </div>
    </main>

    <!-- 底部狀態列 -->
    <footer class="bg-slate-200 border-t border-slate-300 px-4 py-1 text-xs text-slate-600 flex justify-between items-center z-50">
        <div id="statusInfo" class="flex items-center gap-4">
            <span>畫布大小: 900 x 550 像素</span>
            <span id="coordInfo">游標: 0, 0</span>
        </div>
        <div>
            <span>經典小畫家白板 &copy; 2026</span>
        </div>
    </footer>

    <!-- JavaScript 繪圖邏輯 -->
    <script>
        const canvas = document.getElementById('drawCanvas');
        const ctx = canvas.getContext('2d', { willReadFrequently: true });
        const canvasWrapper = document.getElementById('canvasWrapper');
        const imageInput = document.getElementById('imageInput');
        
        // 控制元件
        const pencilToolBtn = document.getElementById('pencilTool');
        const eraserToolBtn = document.getElementById('eraserTool');
        const brushSizeInput = document.getElementById('brushSize');
        const sizeText = document.getElementById('sizeText');
        const paletteGrid = document.getElementById('paletteGrid');
        const customColorInput = document.getElementById('customColor');
        const clearBtn = document.getElementById('clearBtn');
        const undoBtn = document.getElementById('undoBtn');
        const redoBtn = document.getElementById('redoBtn');
        const saveBtn = document.getElementById('saveBtn');
        const coordInfo = document.getElementById('coordInfo');

        // 狀態變數
        let currentTool = 'pencil'; // 'pencil', 'eraser'
        let currentColor = '#000000';
        let currentSize = 3;
        let isDrawing = false;
        let startX = 0, startY = 0;
        let history = [];
        let historyStep = -1;

        // 初始化白板背景為白色
        function initCanvasBackground() {
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            saveState();
        }

        // 狀態管理 (復原/重做)
        function saveState() {
            historyStep++;
            if (historyStep < history.length) {
                history.length = historyStep;
            }
            history.push(canvas.toDataURL());
            updateHistoryButtons();
        }

        function updateHistoryButtons() {
            undoBtn.disabled = historyStep <= 0;
            redoBtn.disabled = historyStep >= history.length - 1;
        }

        // 重新編排的色譜顏色順序
        const colors = [
            '#000000', '#ed1c24', '#ff7f27', '#ffc90e', '#fff200', '#22b14c', '#00a2e8', '#3f48cc', '#a349a4', '#7f7f7f',
            '#ffffff', '#ffaec9', '#ffc89d', '#fae7b5', '#b5e61d', '#99d9ea', '#7092be', '#c8bfe7', '#b97a57', '#c3c3c3'
        ];

        let colorButtons = [];

        colors.forEach((hex) => {
            const btn = document.createElement('button');
            btn.className = 'w-6 h-6 rounded border border-slate-400 shadow-sm transition hover:scale-110 relative';
            btn.style.backgroundColor = hex;
            
            btn.addEventListener('click', () => {
                currentColor = hex;
                updateColorSelection(btn);
            });
            
            paletteGrid.appendChild(btn);
            colorButtons.push({ btn, hex });
        });

        // 更新調色盤選取外框提示
        function updateColorSelection(selectedBtn) {
            colorButtons.forEach(item => {
                item.btn.classList.remove('ring-2', 'ring-blue-600', 'scale-110', 'z-10');
                item.btn.style.borderColor = '#94a3b8';
            });
            if (selectedBtn) {
                selectedBtn.classList.add('ring-2', 'ring-blue-600', 'scale-110', 'z-10');
                selectedBtn.style.borderColor = '#2563eb';
            }
        }

        // 預設選中第一個黑色
        if (colorButtons.length > 0) {
            updateColorSelection(colorButtons[0].btn);
        }

        customColorInput.addEventListener('input', (e) => {
            currentColor = e.target.value;
            updateColorSelection(null);
        });

        // 工具切換
        const allToolBtns = [pencilToolBtn, eraserToolBtn];
        function setActiveTool(selectedBtn, toolName) {
            allToolBtns.forEach(btn => {
                btn.classList.remove('bg-blue-100', 'border-blue-400', 'text-blue-700');
                btn.classList.add('bg-white', 'border-slate-300', 'text-slate-700');
            });
            selectedBtn.classList.remove('bg-white', 'border-slate-300', 'text-slate-700');
            selectedBtn.classList.add('bg-blue-100', 'border-blue-400', 'text-blue-700');
            currentTool = toolName;
            canvas.style.cursor = toolName === 'eraser' ? 'cell' : 'crosshair';
        }

        pencilToolBtn.addEventListener('click', () => setActiveTool(pencilToolBtn, 'pencil'));
        eraserToolBtn.addEventListener('click', () => setActiveTool(eraserToolBtn, 'eraser'));

        brushSizeInput.addEventListener('input', (e) => {
            currentSize = parseInt(e.target.value);
            sizeText.textContent = currentSize;
        });

        // 取得畫布相對座標
        function getMousePos(e) {
            const rect = canvas.getBoundingClientRect();
            return {
                x: Math.floor(e.clientX - rect.left),
                y: Math.floor(e.clientY - rect.top)
            };
        }

        // 繪圖事件監聽
        canvas.addEventListener('mousedown', (e) => {
            const pos = getMousePos(e);
            startX = pos.x;
            startY = pos.y;

            isDrawing = true;
            ctx.beginPath();
            ctx.moveTo(startX, startY);
        });

        canvas.addEventListener('mousemove', (e) => {
            const pos = getMousePos(e);
            coordInfo.textContent = `游標: ${pos.x}, ${pos.y}`;

            if (!isDrawing) return;

            ctx.strokeStyle = currentTool === 'eraser' ? '#ffffff' : currentColor;
            ctx.lineWidth = currentSize;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
            ctx.lineTo(pos.x, pos.y);
            ctx.stroke();
        });

        canvas.addEventListener('mouseup', () => {
            if (isDrawing) {
                isDrawing = false;
                saveState();
            }
        });

        canvas.addEventListener('mouseleave', () => {
            if (isDrawing) {
                isDrawing = false;
                saveState();
            }
        });

        // 圖片上傳功能
        imageInput.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function(event) {
                const img = new Image();
                img.onload = function() {
                    ctx.fillStyle = '#ffffff';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    
                    let w = img.width;
                    let h = img.height;
                    if (w > canvas.width || h > canvas.height) {
                        const ratio = Math.min(canvas.width / w, canvas.height / h);
                        w *= ratio;
                        h *= ratio;
                    }
                    const x = (canvas.width - w) / 2;
                    const y = (canvas.height - h) / 2;
                    ctx.drawImage(img, x, y, w, h);
                    saveState();
                }
                img.src = event.target.result;
            }
            reader.readAsDataURL(file);
        });

        // 新增白板
        clearBtn.addEventListener('click', () => {
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            saveState();
        });

        // 復原與重做
        undoBtn.addEventListener('click', () => {
            if (historyStep > 0) {
                historyStep--;
                let img = new Image();
                img.src = history[historyStep];
                img.onload = () => {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0);
                    updateHistoryButtons();
                }
            }
        });

        redoBtn.addEventListener('click', () => {
            if (historyStep < history.length - 1) {
                historyStep++;
                let img = new Image();
                img.src = history[historyStep];
                img.onload = () => {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0);
                    updateHistoryButtons();
                }
            }
        });

        // 儲存圖片
        saveBtn.addEventListener('click', () => {
            const link = document.createElement('a');
            link.download = 'paint-drawing.png';
            link.href = canvas.toDataURL();
            link.click();
        });

        // 初始化
        initCanvasBackground();
    </script>
</body>
</html>

*提醒邦友,使用第三方服務/API 時,請務必評估資安風險與隱私保護
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言